#include "data_cache.hh"

#include "sqlite3.h"
#include "tsv_parser.hh"

#include <cstdlib>
#include <ctime>
#include <vector>

#if defined(_WIN32)
#include "compat_strptime.h"
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shlobj.h>
#include <shlwapi.h>
#define PATH_DELIM "\\"
#else
#if defined(__APPLE__)
#else
#endif
#define PATH_DELIM "/"
#endif

static void getLocalDataDir(std::vector<std::string> &dirs) {
    char path[1024];
#if defined(_WIN32)
    wchar_t pathw[MAX_PATH];
    if (GetModuleFileNameW(NULL, pathw, MAX_PATH) > 0) {
        PathRemoveFileSpecW(pathw);
        CreateDirectoryW(pathw, NULL);
        if (WideCharToMultiByte(CP_UTF8, 0, pathw, -1, path, 1024, NULL, NULL) > 0) {
            dirs.push_back(path);
        }
    }
    if (SUCCEEDED(SHGetFolderPathW(NULL, CSIDL_APPDATA|CSIDL_FLAG_CREATE, NULL, 0, pathw))) {
        PathAppendW(pathw, L"FreePSS");
        if (WideCharToMultiByte(CP_UTF8, 0, pathw, -1, path, 1024, NULL, NULL) > 0) {
            dirs.push_back(path);
            return;
        }
    }
#elif defined(__APPLE__)
#else
#endif
}

static void *openFile(const std::string &name) {
#if defined(_WIN32)
    wchar_t pathw[MAX_PATH];
    if (MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, pathw, MAX_PATH) <= 0) return NULL;
    HANDLE hFile = CreateFileW(pathw, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) return NULL;
    return (void*)hFile;
#else
#endif
}

static size_t readFile(void *file, void *data, size_t size) {
#if defined(_WIN32)
    DWORD bytesRead;
    if (!ReadFile((HANDLE)file, data, size, &bytesRead, NULL)) return 0;
    return (size_t)bytesRead;
#else
#endif
}

static size_t writeFile(void *file, const void *data, size_t size) {
#if defined(_WIN32)
    DWORD bytesWritten;
    if (!WriteFile((HANDLE)file, data, size, &bytesWritten, NULL)) return 0;
    return (size_t)bytesWritten;
#else
#endif
}

static void closeFile(void *file) {
#if defined(_WIN32)
    CloseHandle((HANDLE)file);
#else
#endif
}

DataCache::DataCache() {
    initInternal();
}

DataCache::~DataCache() {
    freeInternal();
}

void DataCache::loadCache(CacheType type, const std::string &text, uint64_t updateTime) {
    const int typeCols[CacheTypeMax][11] = {
        {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
        {0, 1, 2, 3, 4, 5, 6, -1, 7, 8, -1},
        {0, 1, 2, 3, 4, 5, 6, -1, 7, 8, 9},
    };
    sqlite3_exec(db_, "DROP INDEX `pkg_titleId`", nullptr, nullptr, nullptr);
    sqlite3_stmt *stmt;
    if (sqlite3_prepare_v2(db_, "DELETE FROM `pkg` WHERE `type`=?", -1, &stmt, nullptr) == SQLITE_OK) {
        sqlite3_bind_int(stmt, 1, (int)type);
        sqlite3_step(stmt);
        sqlite3_finalize(stmt);
    }
    sqlite3_exec(db_, "DROP INDEX `pkg_type_titleId`", nullptr, nullptr, nullptr);

    sqlite3_exec(db_, "BEGIN", nullptr, nullptr, nullptr);
    if (sqlite3_prepare_v2(db_, "INSERT INTO `pkg`(`type`,`titleId`,`region`,`name`,`link`,`zRIF`,`contentId`,`lastUpdate`,`originalName`,`fileSize`,`sha256`,`fwVersion`) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)", -1, &stmt, nullptr) == SQLITE_OK) {
        TsvParser parser;
        if (parser.loadFromMemory(text)) {
            size_t rows = parser.rows();
            const int *typeCol = typeCols[type];
            int maxcol = 0;
            for (int i = 0; i < 11; ++i)
                if (typeCol[i] > maxcol) maxcol = typeCol[i];
            for (size_t i = 0; i < rows; ++i) {
                const auto &row = parser.row(i);
                int row_sz = (int)row.size();
                if (row_sz < 5) continue;
                if (row[3].empty() || row[3][0] != 'h' || row[4] == "MISSING") continue;
                sqlite3_reset(stmt);
                sqlite3_bind_int(stmt, 1, (int)type);
                for (int j = 0; j < 11; ++j) {
                    if (typeCol[j] < 0 || typeCol[j] >= row_sz) {
                        sqlite3_bind_text(stmt, j + 2, "", -1, nullptr);
                        continue;
                    }
                    if (j == 6) {
                        struct tm tm;
                        time_t t;
                        if (strptime(row[typeCol[j]].c_str(), "%Y-%m-%d %H:%M:%S", &tm) == NULL) t = 0;
                        else t = mktime(&tm);
                        sqlite3_bind_int64(stmt, j + 2, t);
                    } else
                        sqlite3_bind_text(stmt, j + 2, row[typeCol[j]].c_str(), -1, nullptr);
                }
                sqlite3_step(stmt);
            }
        }
        sqlite3_finalize(stmt);
    }
    sqlite3_exec(db_, "COMMIT", nullptr, nullptr, nullptr);

    cacheTime_[type] = updateTime;
    lastUpdate_[type] = ::time(nullptr);
    if (sqlite3_prepare_v2(db_, "REPLACE INTO `cache_info`(`type`,`cacheTime`,`lastUpdate`) VALUES(?,?,?)", -1, &stmt, nullptr) == SQLITE_OK) {
        sqlite3_bind_int(stmt, 1, (int)type);
        sqlite3_bind_int64(stmt, 2, (int64_t)updateTime);
        sqlite3_bind_int64(stmt, 3, (int64_t)lastUpdate_[type]);
        sqlite3_step(stmt);
        sqlite3_finalize(stmt);
    }

    sqlite3_exec(db_, "CREATE INDEX `pkg_type_titleId` ON `pkg`(`type`, `titleId`)", nullptr, nullptr, nullptr);
    sqlite3_exec(db_, "CREATE INDEX `pkg_titleId` ON `pkg`(`titleId`)", nullptr, nullptr, nullptr);
}

inline Region regionToEnum(const std::string &str) {
    if (str == "JP") return Region::JP;
    if (str == "US") return Region::US;
    if (str == "EU") return Region::EU;
    if (str == "ASIA") return Region::ASIA;
    if (str == "INT") return Region::INT;
    return Region::UNKNOWN;
}

bool DataCache::getPkgList(CacheType type, std::vector<PkgInfo>& list) {
    if (type >= CacheTypeMax) return false;
    sqlite3_stmt *stmt;
    if (sqlite3_prepare_v2(db_, "SELECT `titleId`,`region`,`name`,`link`,`zRIF`,`contentId`,`lastUpdate`,`originalName`,`fileSize`,`sha256`,`fwVersion` FROM `pkg` WHERE `type`=?", -1, &stmt, nullptr) != SQLITE_OK)
        return false;
    sqlite3_bind_int(stmt, 1, (int)type);
    while (sqlite3_step(stmt) == SQLITE_ROW) {
        PkgInfo pi;
        pi.titleId = (const char*)sqlite3_column_text(stmt, 0);
        pi.region = regionToEnum((const char*)sqlite3_column_text(stmt, 1));
        pi.name = (const char*)sqlite3_column_text(stmt, 2);
        pi.link = (const char*)sqlite3_column_text(stmt, 3);
        pi.zRIF = (const char*)sqlite3_column_text(stmt, 4);
        pi.contentId = (const char*)sqlite3_column_text(stmt, 5);
        pi.lastUpdate = sqlite3_column_int64(stmt, 6);
        pi.originalName = (const char*)sqlite3_column_text(stmt, 7);
        pi.fileSize = sqlite3_column_int64(stmt, 8);
        pi.sha256 = (const char*)sqlite3_column_text(stmt, 9);
        pi.fwVersion = (const char*)sqlite3_column_text(stmt, 10);
        list.emplace_back(pi);
    }
    sqlite3_finalize(stmt);
    return true;
}

std::string DataCache::regionToString(Region r) {
    switch (r) {
    case Region::JP: return "JP";
    case Region::US: return "US";
    case Region::EU: return "EU";
    case Region::ASIA: return "ASIA";
    case Region::INT: return "INT";
    }
    return "UNKNOWN";
}

void DataCache::initInternal() {
    std::vector<std::string> dirs;
    getLocalDataDir(dirs);
    if (dirs.empty())
        throw std::bad_exception();
    for (auto &d: dirs) {
        baseDir_ = d;
        configFile_ = baseDir_ + PATH_DELIM + "config.json";
        if (!createOrReadConfig())
            continue;
        cacheFile_ = baseDir_ + PATH_DELIM + "cache.db";
        checkCache();
        return;
    }
    baseDir_.clear();
    configFile_.clear();
    cacheFile_.clear();
    throw std::bad_exception();
}

void DataCache::freeInternal() {
    if (db_) {
        sqlite3_exec(db_, "VACUUM", nullptr, nullptr, nullptr);
        sqlite3_close_v2(db_);
        db_ = nullptr;
    }
}

bool DataCache::createOrReadConfig() {
    void *f = openFile(configFile_);
    if (f == nullptr) return false;
    closeFile(f);
    return true;
}

void DataCache::checkCache() {
    if (sqlite3_open(cacheFile_.c_str(), &db_) != SQLITE_OK) {
        db_ = nullptr;
        return;
    }
    sqlite3_exec(db_, "CREATE TABLE IF NOT EXISTS `cache_info`(`type` INT PRIMARY KEY,`cacheTime` UNSIGNED INT,`lastUpdate` UNSIGNED INT)", nullptr, nullptr, nullptr);
    sqlite3_exec(db_, "CREATE TABLE IF NOT EXISTS `pkg`(`type` INT,`titleId` TEXT,`region` TEXT,`name` TEXT,`link` TEXT,`zRIF` TEXT,`contentId` TEXT PRIMARY KEY,`lastUpdate` UNSIGNED INT,`originalName` TEXT,`fileSize` UNSIGNED INT,`sha256` TEXT,`fwVersion` TEXT)", nullptr, nullptr, nullptr);
    sqlite3_stmt *stmt;
    if (sqlite3_prepare_v2(db_, "SELECT `type`,`cacheTime`,`lastUpdate` FROM `cache_info`", -1, &stmt, nullptr) == SQLITE_OK) {
        while (sqlite3_step(stmt) == SQLITE_ROW) {
            size_t type = sqlite3_column_int(stmt, 0);
            if (type <= CacheTypeMax) {
                cacheTime_[type] = sqlite3_column_int64(stmt, 1);
                lastUpdate_[type] = sqlite3_column_int64(stmt, 2);
            }
        }
        sqlite3_finalize(stmt);
    }
}
